// Direct3D video output, rewritten by regret

/* changelog:
 update 5: partially rewrite (ref: bsnes)
 update 4: add motion-blur (ref: vba-m)
 update 3: rewrite (ref: bsnes)
 update 2: fix screen rotation
 update 1: add osd
*/

//#include "burner.h"

#ifdef _MSC_VER
#pragma comment(lib, "d3d9")
#pragma comment(lib, "d3dx9")
#endif

#include <d3d9.h>
#include <d3dx9.h>

#define D3DVERTEX (D3DFVF_XYZRHW | D3DFVF_TEX1)

//
typedef IDirect3D9 * (__stdcall * Direct3DCreate9Type)(UINT);

class VideoD3D : public Video {
public:
	struct d3dvertex {
		float x, y, z, rhw; // screen coords
		float u, v;         // texture coords
	};

	struct {
		unsigned int t_usage, v_usage;
		unsigned int t_pool,  v_pool;
		unsigned int lock;
		unsigned int filter;
	} flags;

	struct {
		bool dynamic;      // device supports dynamic textures
	} caps;

	IDirect3D9* pD3D;
	D3DPRESENT_PARAMETERS presentParams;
	IDirect3DDevice9* pDevice;
	D3DCAPS9 d3dcaps;
	D3DSURFACE_DESC d3dsd;
	IDirect3DVertexBuffer9* pVertexBuffer;
	d3dvertex* pVertex;
	IDirect3DTexture9* pTexture;
	IDirect3DSurface9* pSurface;

	unsigned int textureWidth;
	unsigned int textureHeight;

	LPD3DXFONT pFont;			// OSD font
	LPD3DXFONT pTinyFont;		// tiny font
	D3DCOLOR osdColor;

	unsigned int gameImageWidth, gameImageHeight;
	unsigned int imageWidth, imageHeight;

	bool lost;
	RECT rd, osdRect;
	unsigned ww, wh, wx, wy;
	bool setview;

	Direct3DCreate9Type Direct3DCreate9Proc;

	void releaseResources()
	{
		RELEASE(pVertexBuffer)
		RELEASE(pSurface)
		RELEASE(pTexture)
	}

	int exit()
	{
		releaseResources();

		exitFont();
		RELEASE(pDevice)
		RELEASE(pD3D)

		return 0;
	}

	int resize(unsigned int width, unsigned int height)
	{
		if (!pDevice)
			return 1;
	    if (d3dcaps.MaxTextureWidth < width || d3dcaps.MaxTextureWidth < height) {
			return 1;
	    }

		RELEASE(pTexture)
		if (FAILED(pDevice->CreateTexture(width, height, 1, flags.t_usage, D3DFMT_X8R8G8B8, (D3DPOOL)flags.t_pool, &pTexture, NULL))) {
			dprintf(("  * Error: Couldn't create texture.\n"));
			return 1;
		}
		if (FAILED(pDevice->SetTexture(0, pTexture))) {
			dprintf(("  * Error: Couldn't set texture.\n"));
			return 1;
		}

		dprintf(("  * Allocated a %i x %i image texture.\n"), width, height);
		return 0;
	}

	int textureInit()
	{
		gameImageWidth = vidSupport.getWidth();
		gameImageHeight = vidSupport.getHeight();

		textureWidth = vidSupport.rounded_power_of_two(gameImageWidth * filter.zoom(filter.filterId));
		textureHeight = vidSupport.rounded_power_of_two(gameImageHeight * filter.zoom(filter.filterId));

		if (resize(textureWidth, textureHeight)) {
			return 1;
		}

		return 0;
	}

	//  Vertex format:
	//
	//  0----------1
	//  |         /|
	//  |       /  |
	//  |     /    |
	//  |   /      |
	//  | /        |
	//  2----------3
	//
	//  (x,y) screen coords, in pixels
	//  (u,v) texture coords, betweeen 0.0 (top, left) to 1.0 (bottom, right)
	bool setVertex(
		unsigned int px, unsigned int py, unsigned int pw, unsigned int ph,
	    unsigned int tw, unsigned int th,
	    unsigned int x, unsigned int y, unsigned int w, unsigned int h
	    )
	{
		if (!pDevice || !pVertexBuffer)
			return false;
		if (FAILED(pVertexBuffer->Lock(0, 4 * sizeof(d3dvertex), (void**)&pVertex, 0)))
			return false;

		pVertex[0].x = pVertex[2].x = (double)(x    ) - 0.5f;
		pVertex[1].x = pVertex[3].x = (double)(x + w) - 0.5f;
		pVertex[0].y = pVertex[1].y = (double)(y    ) - 0.5f;
		pVertex[2].y = pVertex[3].y = (double)(y + h) - 0.5f;

		//Z-buffer and RHW are unused for 2D blit, set to normal values
	    pVertex[0].z = pVertex[1].z = pVertex[2].z = pVertex[3].z = 0.0;
	    pVertex[0].rhw = pVertex[1].rhw = pVertex[2].rhw = pVertex[3].rhw = 1.0;

	    double rw = (double)w / (double)pw * (double)tw;
	    double rh = (double)h / (double)ph * (double)th;
	    pVertex[0].u = pVertex[2].u = (double)(px    ) / rw;
	    pVertex[1].u = pVertex[3].u = (double)(px + w) / rw;
	    pVertex[0].v = pVertex[1].v = (double)(py    ) / rh;
	    pVertex[2].v = pVertex[3].v = (double)(py + h) / rh;

	    pVertexBuffer->Unlock();

		pDevice->SetStreamSource(0, pVertexBuffer, 0, sizeof(d3dvertex));

		return 0;
	}

	// ==> osd for dx9 video output, added by regret
	int createFont()
	{
		if (pFont) {
			return 0;
		}

		HRESULT hr = D3DXCreateFont(pDevice, presentParams.BackBufferHeight / 20,
			0, FW_SEMIBOLD, 1, FALSE,
			DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
			DEFAULT_PITCH || FF_DONTCARE,
			L"Tahoma", &pFont);

		if (FAILED(hr)) {
			return 1;
		}

		if (pTinyFont) {
			return 0;
		}

		hr = D3DXCreateFont(pDevice, presentParams.BackBufferHeight / 22,
			0, FW_SEMIBOLD, 1, FALSE,
			DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, ANTIALIASED_QUALITY,
			DEFAULT_PITCH || FF_DONTCARE,
			L"Tahoma", &pTinyFont);

		if (FAILED(hr)) {
			return 1;
		}

		return 0;
	}

	void exitFont()
	{
		RELEASE(pFont)
		RELEASE(pTinyFont)
	}

	void drawText()
	{
		if (!OSDTimer && !tinyTimer) {
			return;
		}

		if (GetFramesEmulated() > OSDTimer) {
			vidSKillShortMsg();
			vidSKillOSDMsg();
		}
		if (GetFramesEmulated() > tinyTimer) {
			vidSKillTinyMsg();
			vidSKillTinyOSDMsg();
		}

		if (vidFullscreen) {
			osdRect.left = rd.left;
			osdRect.top = rd.top;
			osdRect.right = rd.right - 1;
			osdRect.bottom = rd.bottom - 1;
		} else {
			// fixme: osd postion
			osdRect.left = 0;
			osdRect.top = 0;
			osdRect.right = rd.right - rd.left - 1;
			osdRect.bottom = rd.bottom - rd.top - 1;
		}

		if (OSDTimer) {
			pFont->DrawText(NULL, OSDMsg, -1, &osdRect, DT_RIGHT | DT_TOP, osdColor);
		}
		if (tinyTimer) {
			pTinyFont->DrawText(NULL, tinyMsg, -1, &osdRect, DT_RIGHT | DT_BOTTOM, osdColor);
		}
	}
	// <== osd for dx9 video output

	void updateFilter()
	{
		if (!pDevice)
			return;
		if (lost && !recover())
			return;

		pDevice->SetSamplerState(0, D3DSAMP_MINFILTER, filter.linear ? D3DTEXF_LINEAR : D3DTEXF_POINT);
		pDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, filter.linear ? D3DTEXF_LINEAR : D3DTEXF_POINT);
	}

	bool recover()
	{
		if (!pDevice)
			return false;

		if (lost) {
			if (pFont) {
				pFont->OnLostDevice();
			}
			if (pTinyFont) {
				pTinyFont->OnLostDevice();
			}

			releaseResources();
			if (pDevice->Reset(&presentParams) != D3D_OK) {
				return false;
			}

			if (pFont) {
				pFont->OnResetDevice();
			}
			if (pTinyFont) {
				pTinyFont->OnResetDevice();
			}
		}

		lost = false;

		pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
		pDevice->SetRenderState(D3DRS_LIGHTING, false);
		pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
		pDevice->SetRenderState(D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA);
		pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
		pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
		pDevice->SetVertexShader(NULL);
		pDevice->SetFVF(D3DVERTEX);

		if (FAILED(pDevice->CreateVertexBuffer(4 * sizeof(d3dvertex), flags.v_usage, D3DVERTEX, (D3DPOOL)flags.v_pool, &pVertexBuffer, NULL))) {
			return false;
		}

		if (filter.init(filter.filterId, vidSupport.getRotate())) {
			dprintf(("  * Error: Couldn't initialise pixel filter.\n"));
		}

		// Initialize the buffer surfaces
		if (textureInit()) {
			return false;
		}

		updateFilter();

		// set size
		setview = true;

		clear();
	    return true;
	}

	void clear()
	{
		if (!pDevice)
			return;
		if (lost && !recover())
			return;

		pTexture->GetSurfaceLevel(0, &pSurface);
		if (pSurface) {
			pDevice->ColorFill(pSurface, 0, D3DCOLOR_XRGB(0x00, 0x00, 0x00));
			pSurface->Release();
		}

		//clear primary display and all backbuffers
		for (unsigned i = 0; i < 3; i++) {
			pDevice->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0x00, 0x00, 0x00), 1.0f, 0);
			pDevice->Present(0, 0, 0, 0);
		}
	}

	int init()
	{
		exit();

		if (!Direct3DCreate9Proc) {
			return 1;
		}

		dprintf(("*** Initialising Direct3D 9 blitter.\n"));

		hVidWnd = hScrnWnd;

		pD3D = Direct3DCreate9Proc(D3D_SDK_VERSION);
		if (!pD3D) {
			dprintf(("  * Error: Couldn't initialise Direct3D.\n"));
			return 1;
		}

		// check selected atapter
		if (vidAdapter >= pD3D->GetAdapterCount()) {
			vidAdapter = D3DADAPTER_DEFAULT;
		}

		D3DDISPLAYMODE dm;
		pD3D->GetAdapterDisplayMode(vidAdapter, &dm);

		memset(&presentParams, 0, sizeof(presentParams));
		if (vidFullscreen) {
			bool sizefit = true;
			if (vidWidth > dm.Width || vidHeight > dm.Height) {
				sizefit = false;
			}

			presentParams.BackBufferWidth = sizefit ? vidWidth : dm.Width;
			presentParams.BackBufferHeight = sizefit ? vidHeight : dm.Height;
			presentParams.BackBufferFormat = (vidDepth == 16) ? D3DFMT_R5G6B5 : D3DFMT_X8R8G8B8;
			presentParams.SwapEffect = D3DSWAPEFFECT_FLIP;
			presentParams.BackBufferCount = vidTripleBuffer ? 2 : 1;
			presentParams.hDeviceWindow = hVidWnd;
			presentParams.FullScreen_RefreshRateInHz = dm.RefreshRate;//D3DPRESENT_RATE_DEFAULT;
			presentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
		} else {
			presentParams.SwapEffect             = D3DSWAPEFFECT_FLIP;//D3DSWAPEFFECT_DISCARD;
			presentParams.hDeviceWindow          = hVidWnd;
			presentParams.PresentationInterval   = D3DPRESENT_INTERVAL_IMMEDIATE;//D3DPRESENT_INTERVAL_DEFAULT;
			presentParams.Windowed               = TRUE;
			presentParams.BackBufferFormat       = D3DFMT_UNKNOWN;
		}

		if (FAILED(pD3D->CreateDevice(vidAdapter, D3DDEVTYPE_HAL, hVidWnd,
			D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE, &presentParams, &pDevice))) {
			if (FAILED(pD3D->CreateDevice(vidAdapter, D3DDEVTYPE_HAL, hVidWnd,
				D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE, &presentParams, &pDevice))) {

				dprintf(("  * Error: Couldn't create Direct3D device.\n"));

				if (vidFullscreen) {
					FBAPopupAddText(PUF_TEXT_DEFAULT, MAKEINTRESOURCE(IDS_ERR_UI_FULL_PROBLEM), presentParams.BackBufferWidth, presentParams.BackBufferHeight, presentParams.BackBufferFormat, presentParams.FullScreen_RefreshRateInHz);
					if (presentParams.BackBufferWidth != 320 && presentParams.BackBufferHeight != 240) {
						FBAPopupAddText(PUF_TEXT_DEFAULT, MAKEINTRESOURCE(IDS_ERR_UI_FULL_CUSTRES));
					}
					FBAPopupDisplay(PUF_TYPE_ERROR);
				}

				return 1;
			}
		}

		vidScrnWidth = dm.Width; vidScrnHeight = dm.Height;
		vidScrnDepth = (dm.Format == D3DFMT_R5G6B5) ? 16 : 32;

		// detect capabilities
		pDevice->GetDeviceCaps(&d3dcaps);

		caps.dynamic = bool(d3dcaps.Caps2 & D3DCAPS2_DYNAMICTEXTURES);
		if (caps.dynamic) {
			flags.t_usage = D3DUSAGE_DYNAMIC;
			flags.v_usage = D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC;
			flags.t_pool  = D3DPOOL_DEFAULT;
			flags.v_pool  = D3DPOOL_DEFAULT;
			flags.lock    = D3DLOCK_NOSYSLOCK | D3DLOCK_DISCARD;
		} else {
			flags.t_usage = 0;
			flags.v_usage = D3DUSAGE_WRITEONLY;
			flags.t_pool  = D3DPOOL_MANAGED;
			flags.v_pool  = D3DPOOL_MANAGED;
			flags.lock    = D3DLOCK_NOSYSLOCK;
		}

		lost = false;
		if (!recover()) {
			return 1;
		}

		// Create osd font
		createFont();

		dprintf(("  * Initialisation complete: %.2lfMB texture memory free (total).\n"), (double)pDevice->GetAvailableTextureMem() / (1024 * 1024));
		dprintf(("    Displaying and rendering in %i-bit mode, emulation running in %i-bit mode.\n"), vidScrnDepth, 32);
		if (vidFullscreen) {
			dprintf("    Running in fullscreen mode (%i x %i), ", vidScrnWidth, vidScrnHeight);
			dprintf("using a %s buffer.\n", vidTripleBuffer ? L"triple" : L"double");
		}

		return 0;
	}

	int scale(void* rect, unsigned width, unsigned height)
	{
		return vidSupport.scaleImage(rect, width, height);
	}

	int render()
	{
		if (!pDevice)
			return 1;
		if (lost && !recover())
			return 1;

		getClientScreenRect(hVidWnd, &rd);

		ww = rd.right - rd.left;
		wh = rd.bottom - rd.top;

		scale(&rd, gameImageWidth, gameImageHeight);

		unsigned newImageWidth  = rd.right - rd.left;
		unsigned newImageHeight = rd.bottom - rd.top;

		if (newImageWidth != 0 && newImageHeight != 0
			&& (imageWidth != newImageWidth || imageHeight != newImageHeight)) {
			imageWidth  = newImageWidth;
			imageHeight = newImageHeight;
			init(); // reinit
			return 0;
		}

		if (setview) {
			setview = false;

			unsigned width = gameImageWidth;
			unsigned height = gameImageHeight;

			width *= filter.zoom(filter.filterId);
			height *= filter.zoom(filter.filterId);

			wx = (ww - (rd.right - rd.left)) >> 1;
			wy = (wh - (rd.bottom - rd.top)) >> 1;

			setVertex(0, 0, width, height, textureWidth, textureHeight, wx, wy, imageWidth, imageHeight);
		}

		pTexture->GetLevelDesc(0, &d3dsd);
		if (FAILED(pTexture->GetSurfaceLevel(0, &pSurface))) {
			dprintf(("  * Error: Couldn't get texture surface.\n"));
			return 1;
		}

		// Copy the game image onto a texture for rendering
		D3DLOCKED_RECT d3dlr;
		pSurface->LockRect(&d3dlr, 0, flags.lock);

		unsigned pitch = d3dlr.Pitch;
		unsigned char* pd = (unsigned char*)d3dlr.pBits;

		filter.apply(pd, pitch);

		pSurface->UnlockRect();
		pSurface->Release();

		pDevice->BeginScene();

		pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

		drawText(); // draw osd text

		pDevice->EndScene();

		return 0;
	}

	int frame(bool redraw)
	{
		render();
		return 0;
	}

	int paint(int validate)
	{
		if (!pDevice)
			return 1;

		if (!vidFullscreen) {
			RECT rect = { 0, 0, 0, 0 };
			getClientScreenRect(hVidWnd, &rect);

			scale(&rect, vidSupport.getWidth(), vidSupport.getHeight());
			if (rd.left != rect.left || rd.right != rect.right
				|| rd.top != rect.top || rd.bottom != rect.bottom) {
				validate |= 2;
			}
		}

		if (validate & 2) {
			render();
		}

		if (vidVSync) {
			while(true) {
				D3DRASTER_STATUS status;
				pDevice->GetRasterStatus(0, &status);
				if (status.InVBlank)
					break;
			}
		}

	 	if (pDevice->Present(NULL, NULL, 0, NULL) == D3DERR_DEVICELOST) {
			lost = true;
	 	}

		if (validate & 1) {
			ValidateRect(hVidWnd, 0);
		}

		return 0;
	}

	int get(void* info)
	{
		InterfaceInfo* pInfo = (InterfaceInfo*)info;
		if (!pInfo) {
			return 1;
		}

		if (vidFullscreen) {
			if (vidTripleBuffer) {
				IntInfoAddStringModule(pInfo, L"Using a triple buffer");
			}
		}

		if (filter.linear) {
			IntInfoAddStringModule(pInfo, L"Applying linear filter");
		} else {
			IntInfoAddStringModule(pInfo, L"Applying pointer filter");
		}
		return 0;
	}

	int set(const wchar_t* name, const void* value)
	{
		if (!wcscmp(name, L"Filter")) {
			updateFilter();
		}
		return 0;
	}

	VideoD3D() {
		pD3D = NULL;
		pDevice = NULL;
		pVertexBuffer = NULL;
		pVertex = NULL;
		pTexture = NULL;
		pSurface = NULL;

		textureWidth = 0;
		textureHeight = 0;

		pFont = NULL;
		pTinyFont = NULL;
		osdColor = D3DCOLOR_ARGB(0xFF, 0xFF, 0xFF, 0xFF);

		gameImageWidth = gameImageHeight = 0;
		imageWidth = imageHeight = 0;

		lost = true;
		rd.left = rd.right = rd.top = rd.bottom = 0;
		ww = wh = wx = wy = 0;
		setview = false;

		Direct3DCreate9Proc = NULL;

		// Get pointer to Direct3D
		HMODULE handle = LoadLibrary(L"d3d9.dll");
		if (handle) {
			Direct3DCreate9Proc = (Direct3DCreate9Type)GetProcAddress(handle, "Direct3DCreate9");
		}
	}

	~VideoD3D() {
		exit();
	}
};

#undef D3DVERTEX
